package com.codrim.cs.data.server.subservice;

import com.codrim.common.utils.json.JsonMapper;
import com.codrim.cs.data.rpc.dto.param.ReportStatisticParamDTO;
import com.codrim.cs.data.rpc.enums.TimeDimension;
import com.codrim.cs.data.server.dao.entity.AbstractStatCsEntity;
import com.codrim.cs.data.server.dao.entity.ReportRecordDO;
import com.codrim.cs.data.server.dao.entity.StatCsGeneralDO;
import com.codrim.cs.data.server.dao.mapper.ReportRecordMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsAdvAffMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsAdvAllMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsAdvOfferMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsAffAllMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsAffOfferMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsGeneralMapper;
import com.codrim.cs.data.server.dao.mapper.StatCsMapper;
import com.codrim.cs.data.server.dao.param.StatCsUniqueKey;
import com.codrim.cs.data.server.rpc.report.AbstractStatDataHelper;
import com.codrim.cs.data.server.rpc.report.StatCsAdvAffHelper;
import com.codrim.cs.data.server.rpc.report.StatCsAdvAllHelper;
import com.codrim.cs.data.server.rpc.report.StatCsAdvOfferHelper;
import com.codrim.cs.data.server.rpc.report.StatCsAffAllHelper;
import com.codrim.cs.data.server.rpc.report.StatCsAffOfferHelper;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.Resource;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * @author Liang.Zhuge
 * @date 22/02/2018
 */
@Service
@Slf4j
public class ReportSubService {
    @Resource private TimeZoneSubService timeZoneSubService;

    @Resource private ReportRecordMapper reportRecordMapper;
    @Resource private StatCsAffAllMapper statCsAffAllMapper;
    @Resource private StatCsAdvAllMapper statCsAdvAllMapper;
    @Resource private StatCsAdvAffMapper statCsAdvAffMapper;
    @Resource private StatCsAdvOfferMapper statCsAdvOfferMapper;
    @Resource private StatCsAffOfferMapper statCsAffOfferMapper;
    @Resource private StatCsGeneralMapper statCsGeneralMapper;

    @Async
    @Transactional(rollbackFor = Exception.class)
    public void doDailyReportStatistic(ReportStatisticParamDTO param) {
        log.info("Daily report statistic, param:{}", JsonMapper.nonEmptyMapper().toJson(param));
        final long begin = System.currentTimeMillis();

        Assert.isTrue(param.getBeginTime() > 0, "Invalid begin time");
        Assert.isTrue(param.getEndTime() > 0, "Invalid end time");
        final List<ReportStatisticParamDTO.ZoneSdate> zoneSDates = param.getZoneSdateList();
        Assert.isTrue(CollectionUtils.isNotEmpty(zoneSDates));

        synchronized (lockString(param).intern()) {
            final long constTime;
            if (!reportReportExists(param)) {
                zoneSDates.forEach(zoneSDate -> statisticReportForZone(param, zoneSDate));

                constTime = System.currentTimeMillis() - begin;

                saveReportRecord(param, zoneSDates, constTime);
            } else {
                constTime = -1;
                log.warn("Daily report statistic, report record exists");
            }
            log.info("Statistic daily report finish, cost: {}", constTime);
        }
    }

    private String lockString(ReportStatisticParamDTO param) {
        return String.format("%s_%s_%s", param.getBeginTime(), param.getEndTime(), sZones(param.getZoneSdateList()));
    }

    private boolean reportReportExists(ReportStatisticParamDTO param) {
        List<ReportRecordDO> reportRecords = reportRecordMapper.findAllByBeginTimeAndEndTimeAndZone(
                param.getBeginTime(), param.getEndTime(), sZones(param.getZoneSdateList()));
        return CollectionUtils.isNotEmpty(reportRecords);
    }

    private void saveReportRecord(ReportStatisticParamDTO param,
            List<ReportStatisticParamDTO.ZoneSdate> zoneSDates,
            long constTime) {
        ReportRecordDO reportRecordDO = new ReportRecordDO();
        reportRecordDO.setBeginTime(param.getBeginTime());
        reportRecordDO.setEndTime(param.getEndTime());

        String join = sZones(zoneSDates);
        reportRecordDO.setZone(join);

        reportRecordDO.setCreateTime(new Date());
        reportRecordDO.setCostInMs(constTime);
        reportRecordMapper.insert(reportRecordDO);
    }

    private String sZones(List<ReportStatisticParamDTO.ZoneSdate> zoneSDates) {
        final List<String> sZones = zoneSDates.stream()
                .map(item -> timeZoneSubService.prefixTableNameForTimeZone(item.getZone()))
                .collect(Collectors.toList());
        return StringUtils.join(sZones, ",");
    }

    private <Entity extends AbstractStatCsEntity<Key>, Key extends StatCsUniqueKey> void statisticReportForZone(
            ReportStatisticParamDTO param, ReportStatisticParamDTO.ZoneSdate zoneSdate) {
        final int sdate = zoneSdate.getSDate();
        final int zone = zoneSdate.getZone();
        log.info("Statistic daily report, sdate:{}, sZone:{}", sdate, zone);
        log.info("Statistic daily report, param:{}, zoneSdate:{}", JsonMapper.nonEmptyMapper().toJson(param),
                JsonMapper.nonEmptyMapper().toJson(zoneSdate));
        final String sZone = timeZoneSubService.prefixTableNameForTimeZone(zone);


        final List<HelperAndMapper<Entity, Key>> helperAndMappers = helperAndMappers();
        /* 处理general数据 */
        final List<TimeDimensionWithSdate> allTimeDimensions = ReportUtils.toMultiTimeDimensionWithSdate(String.valueOf(sdate), zone);
        allTimeDimensions.forEach(dimension ->
                param.getData().forEach(item -> {
                    insertOrMergeIfExist(statCsGeneralMapper, new StatCsGeneralDO(dimension.getSdate(), item), sZone,
                            dimension.getTimeDimension());
                    helperAndMappers.forEach(hm -> hm.getHelper().addStatisticData(dimension, item));
                })
        );

        /* 处理其它纬度数据 */
        helperAndMappers.forEach(helperAndMapper -> {
            AbstractStatDataHelper<Entity> helper = helperAndMapper.getHelper();
            log.info("Insert or update data, [{}], mapDataSize:{}, sZone:{}", helper.getClass().getName(),
                    helper.mapDataSize(), sZone);
            helper.elements().forEach(item -> item.getValue().values().forEach(entity -> insertOrMergeIfExist(helperAndMapper.getMapper(), entity, sZone, item.getKey())));
        });
    }

    private <Entity extends AbstractStatCsEntity<Key>, Key extends StatCsUniqueKey> void insertOrMergeIfExist(
            StatCsMapper<Entity, Key> mapper, Entity entity, String sZone, TimeDimension timeDimension) {
        Key uniqueKey = entity.createUniqueKey();
        Entity entityInDB = mapper.findByKey(uniqueKey, sZone, timeDimension.key);
        log.info("unique key:{}, entity exit:{}", JsonMapper.nonEmptyMapper().toJson(uniqueKey), entityInDB != null);
        if (entityInDB != null) {
            entityInDB.plus(entity);
            mapper.updateById(entityInDB, entityInDB.getId(), sZone, timeDimension.key);
        } else {
            log.debug("Not found data in databases will insert");
            mapper.insert(entity, sZone, timeDimension.key);
        }
    }

    @Data
    private static class HelperAndMapper<Entity extends AbstractStatCsEntity<Key>, Key extends StatCsUniqueKey> {
        StatCsMapper<Entity, Key> mapper;
        AbstractStatDataHelper<Entity> helper;

        HelperAndMapper(StatCsMapper<Entity, Key> mapper, AbstractStatDataHelper<Entity> helper) {
            this.mapper = mapper;
            this.helper = helper;
        }
    }

    @SuppressWarnings("unchecked")
    private <Entity extends AbstractStatCsEntity<Key>, Key extends StatCsUniqueKey> List<HelperAndMapper<Entity, Key>> helperAndMappers() {
        final List<HelperAndMapper<Entity, Key>> helperAndMappers = new ArrayList<>();
        helperAndMappers.add(new HelperAndMapper(statCsAdvAllMapper, new StatCsAdvAllHelper()));
        helperAndMappers.add(new HelperAndMapper(statCsAffAllMapper, new StatCsAffAllHelper()));
        helperAndMappers.add(new HelperAndMapper(statCsAdvAffMapper, new StatCsAdvAffHelper()));
        helperAndMappers.add(new HelperAndMapper(statCsAdvOfferMapper, new StatCsAdvOfferHelper()));
        helperAndMappers.add(new HelperAndMapper(statCsAffOfferMapper, new StatCsAffOfferHelper()));
        return helperAndMappers;
    }
}
